﻿/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * This class will add support for Multipart mime-messages loaded from Stream objects.
 * The data is loaded as a StreamContent but the headers are scanned and attached to 
 * the "HttpContent".
 * 
 * After the HttpContent is created, you may use the HttpContentMultipartExtensions to 
 * check if the Content is a "Multipart" mime-message or not.
 *  
 * User: Marian Pascalau
 * Date: 25.03.2015
 * Time: 15:03
 * 
 */

#define Patch_Headers_For_DeltaSync

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

using System.Net.Http;
using System.Net.Http.Headers;

namespace PM.Net.Http
{
    /// <summary>
    /// Description of MimeParser.
    /// </summary>
    public static class MimeParser {
        private const int MAX_HEADER_SIZE = 512 * 1024; // 512 kb...
        
        private static object InvokeInstanceMethod(object obj, string name, params object[] args) {
            Type type = obj.GetType();
            foreach (MethodInfo method in type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
                if (method.Name == name && !method.IsStatic)
                    return method.Invoke(obj, args);
            throw new ArgumentOutOfRangeException("Method-Name", "Method not found: " + name);
        }
        private static object InvokeStaticMethod(Type type, string name, params object[] args) {
            foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
                if (method.Name == name && method.IsStatic)
                    return method.Invoke(null, args);
            throw new ArgumentOutOfRangeException("Method-Name", "Method not found: " + name);
        }
        private static object GetField(object obj, string name) {
            Type type = obj.GetType();
            foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
                if (field.Name == name)
                    return field.GetValue(obj);
            throw new ArgumentOutOfRangeException("Field-Name", "Field not found: " + name);
        }
        
        #if Patch_Headers_For_DeltaSync
        private class HttpHeadersEx : HttpHeaders {
            public HttpHeadersEx() {}
        }
        #endif        
        
        public static StreamContent CreateMimeContent(Stream content, int maxHeaderSize = MAX_HEADER_SIZE) {
            if (!content.CanSeek)
                throw new ArgumentException("Non-Navigable streams cannot be used as source for MimeContent");

            StreamContent mimecontent = new StreamContent(content);
            
            #if Patch_Headers_For_DeltaSync
            // This patch is important in order to avoid a problem with Content-Type header values. 
            // Because the parser of "content-type" header values is too restrictive, we have sometimes 
            // to extra-quote the header parameter-values (specialy for boundary) in order to allow a  
            // much flexible HttpContent processing! 
            // This operation is done by loading the HttpContent-MimeHeader into a temporary collection 
            // and process the content-type entry in a much flexible way.            
            var hdrs = new HttpHeadersEx();
            #else
            var hdrs = mimecontent.Headers;
            #endif
            
            // Create a new instance of a mime-header parser...
            // Because the parser is declared as "internal" we need to grab it using Reflection :P
            var ipos = content.Seek(0, SeekOrigin.Current);
            var type = typeof(HttpContentMultipartExtensions).Assembly.
                GetType("System.Net.Http.Formatting.Parsers.InternetMessageFormatHeaderParser", true);
            var cons = type.GetConstructor(new Type[] { typeof(System.Net.Http.Headers.HttpHeaders), typeof(int) });
            if (cons == null)
                throw new NotSupportedException("Current \"internal\" HttpHeaderParser is not supported");
            var prsr = cons.Invoke(new object[] { hdrs, 4096 }); // content.Headers
            
            // Check header only in the first chunk (default=512Kb) of data...
            var data = new byte[maxHeaderSize];
            var args = new object[] { data, content.Read(data, 0, data.Length), 0 };
            var nval = InvokeInstanceMethod(prsr, "ParseBuffer", args);
            if ((int)nval != 1) // 1 == Success
                throw new InvalidOperationException("The MimeHeader cannot be processed (result=" + nval.ToString() + ")");
            
            // Restore stream position (this should make sure that the file mime-content is complete)... 
            content.Position = ipos /*+ (int)args[2]*/;
    
            #if Patch_Headers_For_DeltaSync
            foreach (KeyValuePair<string, IEnumerable<string>> rec in hdrs) {
                if (string.Compare(rec.Key, "content-type", true) == 0) {
                    List<string> list = new List<string>(rec.Value);
                    if (list.Count == 1) {
                        string value = list[0];
                        var parts = value.Split(new char[] { ';' }, 2);
                        var ct    = new MediaTypeHeaderValue(parts[0].Trim());
                        for (string temp = parts.Length > 1 ? parts[1].Trim() : null; !string.IsNullOrEmpty(temp); temp = parts.Length > 1 ? parts[1].Trim() : null) {
                            parts = temp.Split(new char[] { ';' }, 2);
                            
                            var prts2     = parts[0].Split(new char[] { '=' }, 2);
                            var namevalue = new NameValueHeaderValue(prts2[0]);
                            if (prts2.Length > 1) {
                                try {
                                    namevalue.Value = prts2[1];
                                } catch (FormatException) {
                                    if (prts2[1].IndexOf('"') < 0)
                                        // sometimes, the namevalue parser is too restrictive and in oder to avoid extra checks, 
                                        // it helps to quote the string-value (when value was not previously quoted)...
                                        namevalue.Value = '"' + prts2[1] + '"';
                                    else
                                        throw;
                                }
                            }
                            ct.Parameters.Add(namevalue);
                        }
                        
                        // Properly set the ContentType header (boundary parameter). This will enable the support for
                        // HttpContent Multipart Extensions!                         
                        mimecontent.Headers.ContentType = ct;
                    } 
                    else
                        // Silently, copy the header value...
                        mimecontent.Headers.TryAddWithoutValidation(rec.Key, rec.Value);
                }
                else
                    // Silently, copy the header value...
                    mimecontent.Headers.TryAddWithoutValidation(rec.Key, rec.Value);
            }
            #endif
            
            return mimecontent;
        }
        
        public static IEnumerable<HttpContent> GetContents(HttpContent content) {
            if (content.IsMimeMultipartContent())
                return content.ReadAsMultipartAsync().Result.Contents;
            return new HttpContent[] { content };
        }
        
        /*
        public delegate void OnProcessPart(System.Net.Http.HttpContent content);
        public static void ProcessMultipartContentStream(Stream stream, OnProcess process) {
        	
        	// The MimeParser.Parse method will read the StreamContent and will parse the Mime header...
        	// As result of this operation, the HttpContent Extensions will allow queries for Multipart 
        	// information: mimecontent.IsMimeMultipartContent() will return True for MultipartContents
        	// ... the same time, mimecontent.ReadAsMultipartAsync().Result.Contents will return each part 
        	//     of the HttpContent Multipart collection!
        	
        	using (var mimecontent = PM.Net.Http.MimeParser.Parse(stream)) {
                if (mimecontent.IsMimeMultipartContent())
            	    foreach (HttpContent part in mimecontent.ReadAsMultipartAsync().Result.Contents) {
					    process(part); // to access the part as stream use: part.ReadAsStreamAsync().Result 
				    }
             	Console.WriteLine("Content decoded!");
			}
        }
        */
    }
}
